/*
 *
 * Created on: 25.06.2012
 *
 * qdaemonapplication.cpp
 * This file is part of KOBUS
 * 
 * Copyright (C) 2012 - Ivan Penkin, MIPT
 * grek.penkin@gmail.com
 *
 * KOBUS is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * KOBUS is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with KOBUS; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, 
 * Boston, MA  02110-1301  USA
 *  
 */

#include <qdaemonapplication.h>
#include <log.h>

#include <libgen.h>
#include <errno.h>
#include <pwd.h>
#include <grp.h>

#include <sys/file.h>
#include <sys/types.h>

using namespace kobus;

int QDaemonApplication::sigFdIn  = -1;
int QDaemonApplication::sigFdOut = -1;
int QDaemonApplication::parentExitStatus = -1;


void QDaemonApplication::handleSignal() {

  sigNotifier -> setEnabled(false);

  int sigNo = 0;
  ::read(sigFdOut, &sigNo, sizeof(sigNo));

  Log log;
  log.writeDebug(Log::EXTRA, "handleSignal(): " + QString::number(sigNo, 10));

  switch (sigNo) {
  case SIGINT  :
    emit intSignal();
    break;
  case SIGQUIT :
    emit quitSignal();
    break;
  case SIGILL  :
    emit illSignal();
    break;
  case SIGABRT :
    emit abrtSignal();
    break;
  case SIGFPE  :
    emit fpeSignal();
    break;
  case SIGPIPE :
    emit pipeSignal();
    break;
  case SIGALRM :
    emit alrmSignal();
    break;
  case SIGTERM :
    emit termSignal();
    break;
  case SIGUSR1 :
    emit usr1Signal();
    break;
  case SIGUSR2 :
    emit usr2Signal();
    break;
  case SIGCHLD :
    emit chldSignal();
    break;
  case SIGCONT :
    emit contSignal();
    break;
  }

  sigNotifier -> setEnabled(true);
}


void QDaemonApplication::initialize (char * argv_0, const QString & lockFDir) {

  d_userName.clear();
  d_groupName.clear();
  d_child = false;

  setApplicationName(basename(argv_0));

  lockFile().setFileName(
                    QDir::toNativeSeparators(
                                lockFDir + "/" + applicationName() + ".pid"));
}

void QDaemonApplication::setSyncSignals() {

  signal(SIGCHLD, QDaemonApplication::parentsSigHandler);
  signal(SIGUSR1, QDaemonApplication::parentsSigHandler);
  signal(SIGALRM, QDaemonApplication::parentsSigHandler);
}


void QDaemonApplication::setSignals() {

  signal(SIGTSTP, SIG_IGN);
  signal(SIGTTOU, SIG_IGN);
  signal(SIGTTIN, SIG_IGN);
  signal(SIGHUP,  SIG_IGN);
  signal(SIGINT,  unixSignalHandler);
  signal(SIGQUIT, unixSignalHandler);
  signal(SIGILL,  unixSignalHandler);
  signal(SIGABRT, unixSignalHandler);
  signal(SIGFPE,  unixSignalHandler);
  signal(SIGPIPE, unixSignalHandler);
  signal(SIGALRM, unixSignalHandler);
  signal(SIGTERM, unixSignalHandler);
  signal(SIGUSR1, unixSignalHandler);
  signal(SIGUSR2, unixSignalHandler);
  signal(SIGCHLD, unixSignalHandler);
  signal(SIGCONT, unixSignalHandler);
}

int QDaemonApplication::setGidUid(QDaemonApplication * instance) {

  Log log;

  if (!instance -> userName().isEmpty()) {
    passwd * pw = getpwnam(instance -> userName_char());
    log.write(Log::LOG_NOTICE, "Setting user to %s", instance -> userName_char());
    if (pw) {
      if ( setuid(pw -> pw_uid) < 0) {
        log.write(Log::LOG_ERR, "Couldn't set the user:%s", strerror(errno));
        return -1;
      };
    } else {
      log.write(Log::LOG_ERR, "Couldn't find the user.");
      return -1;
    }
  }

  if (!instance -> groupName().isEmpty()) {
    group * gr = getgrnam(instance -> groupName_char());
    log.write(
          Log::LOG_NOTICE, "Setting group to %s", instance -> groupName_char());
    if (gr) {
      if (setgid(gr -> gr_gid) < 0) {
        log.write(Log::LOG_ERR, "Couldn't set group:%s", strerror(errno));
        return -1;
      };
    } else {
      log.write(Log::LOG_ERR, "Couldn't find the group.");
      return -1;
    }
  }
  return 1;
}


int QDaemonApplication::setSid() {

  Log log;

  if (setsid() < 0) {
    log.write(
           Log::LOG_ERR, "unable to create a new session, error No. %d (%s)",
           errno, strerror(errno));
    return -1;
  } else return 1;
}

int QDaemonApplication::chDir() {

  Log log;

  if (chdir("/") < 0) {
    log.write(
          Log::LOG_ERR, "unable to change directory to %s, code %d (%s)",
          "/", errno, strerror(errno));
    return -1;
  } else return 1;
}

QDaemonApplication::QDaemonApplication(int & argc, char ** argv):
  QCoreApplication(argc, argv) {

  initialize(*argv, "/var/run");
}


QDaemonApplication::QDaemonApplication(
                         int & argc, char ** argv, const QString & lockFDir):
  QCoreApplication(argc, argv) {

  initialize(*argv, lockFDir);
}


QDaemonApplication::~QDaemonApplication() {

  emit termination();

  Log log;

  if (child()) {
    flock(lockFile().handle(), LOCK_UN);

    log.writeDebug(
          Log::DEEP, "File %s unlocked. Process terminated.",
          lockFile().fileName().toLocal8Bit().data());

    close(sigFdIn);
    close(sigFdOut);

    delete sigNotifier;
  }

  log.writeDebug(Log::EXTRA, "~QDaemonApplication()");
}


int QDaemonApplication::exec() {

  QDaemonApplication * instance = QDaemonApplication::instance();
  if (!instance)
    return EXIT_FAILURE;

  Log log;

  if (!instance -> lockFile().open(QIODevice::ReadWrite)) {
    printf("Unable to open/create lock file '%s', error No. %d (%s)",
        instance -> lockFile().fileName().toLocal8Bit().data(),
        errno, strerror(errno));
    log.write(
      Log::LOG_ERR, "Unable to open/create lock file '%s', error No. %d (%s)",
      instance -> lockFile().fileName().toLocal8Bit().data(),
      errno, strerror(errno));
    return EXIT_FAILURE;
  };

  if (flock(instance -> lockFile().handle(), LOCK_EX | LOCK_NB) < 0) {
    if (errno == EWOULDBLOCK) {
      printf("Process already running, solve the problem by hand\n");
      log.write(Log::LOG_NOTICE, "Process already running");
    } else {
      log.write(
            Log::LOG_ERR, "Unable to lock file '%s', error No. %d (%s)",
            instance -> lockFile().fileName().toLocal8Bit().data(),
            errno, strerror(errno));
    };
    return EXIT_FAILURE;
  };

  // TODO: open/reopen lockfile problem
  /* dangerous section */
  instance -> lockFile().close();
  instance -> lockFile().open(QIODevice::WriteOnly);
  flock(instance -> lockFile().handle(), LOCK_EX | LOCK_NB);
  /* dangerous section */

  if (setGidUid(instance) < 0 )
    return EXIT_FAILURE;

  setSyncSignals();

  pid_t pid = fork();

  if (pid < 0) {
    log.write(
           Log::LOG_ERR, "Unable to fork process, error No. %d (%s)",
           errno, strerror(errno));
    return EXIT_FAILURE;
  }

  if (pid > 0) {
    /* Waiting for confirmation from the child via SIGTERM or SIGCHLD, or
    for two seconds to elapse (SIGALRM). */
    parentExitStatus = EXIT_FAILURE;

    alarm(2);
    if (sleep(3) == 0) return EXIT_FAILURE;

    return parentExitStatus;
  }

  instance -> d_child = true;
  pid_t parent = getppid();

  umask(0);

  if (setSid() < 0)
    return EXIT_FAILURE;

  if (chDir() < 0)
    return EXIT_FAILURE;

  int pair[2];
  if (::socketpair(AF_UNIX, SOCK_STREAM, 0, pair)) {
    log.write(
           Log::LOG_ERR, "unable to create socketpair, code %d (%s)",
           errno, strerror(errno));
    return EXIT_FAILURE;
  }
  sigFdIn  = pair[0];
  sigFdOut = pair[1];

  instance -> lockFile().write(QString().number(getpid(), 10).toLocal8Bit());
  instance -> lockFile().flush();

  kill(parent, SIGUSR1);

  /* Cancel certain signals */
  setSignals();

  instance -> sigNotifier =
      new  QSocketNotifier(sigFdOut, QSocketNotifier::Read, instance);
  connect(
      instance -> sigNotifier, SIGNAL(activated(int)),
      instance,                SLOT(handleSignal()),
      Qt::QueuedConnection);

  close(0);
  close(1);
  close(2);

  return QCoreApplication::exec();
}


QDaemonApplication * QDaemonApplication::instance() {

  return qobject_cast<QDaemonApplication *>(QCoreApplication::instance());
}


void QDaemonApplication::parentsSigHandler(int sigNo) {

  Log log;

  switch(sigNo) {
  case SIGALRM:
    log.write(
          Log::LOG_ERR,
          "Unable to normally fork process: no "
          "response from the child");
    parentExitStatus = EXIT_FAILURE;
    break;

  case SIGUSR1:
    parentExitStatus = EXIT_SUCCESS;
    break;
  case SIGCHLD:
    log.write(
          Log::LOG_ERR, "Unable to normally fork process: "
         "child died");
    parentExitStatus = EXIT_FAILURE;
    break;
  }
};


void QDaemonApplication::unixSignalHandler(int sigNo) {

  ::write(sigFdIn, &sigNo, sizeof(sigNo));
}


bool QDaemonApplication::setUserName(const QString & uName) {

  passwd * pw = getpwnam(uName.toLocal8Bit().data());
  if (!pw) return false;

  d_userName = uName;
  const_cast<QByteArray &>(d_userName_char) = userName().toLocal8Bit();

  return true;
}


bool QDaemonApplication::setGroupName(const QString & gName) {

  group * gr = getgrnam(gName.toLocal8Bit().data());
  if (!gr) return false;

  d_groupName = gName;
  const_cast<QByteArray &>(d_groupName_char) = groupName().toLocal8Bit();
  return true;
}


const QString & QDaemonApplication::userName() const {

  return d_userName;
}


char const * QDaemonApplication::userName_char() const {

  return d_userName_char.data();
}


const QString & QDaemonApplication::groupName() const {

  return d_groupName;
}


char const * QDaemonApplication::groupName_char() const {

  return d_groupName_char.data();
}


const QFile & QDaemonApplication::lockFile() const {

  return d_lockFile;
}


QFile & QDaemonApplication::lockFile() {

  return d_lockFile;
}


bool QDaemonApplication::child() const {

  return d_child;
}
